/*
 * Copyright (C) 2001-2016 Food and Agriculture Organization of the
 * United Nations (FAO-UN), United Nations World Food Programme (WFP)
 * and United Nations Environment Programme (UNEP)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 *
 * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
 * Rome - Italy. email: geonetwork@osgeo.org
 */

package org.fao.geonet.kernel.search.log;

import com.google.common.base.Optional;
import jeeves.server.context.ServiceContext;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.fao.geonet.constants.Geonet;
import org.fao.geonet.utils.Log;
import org.springframework.beans.factory.annotation.Value;

import java.util.ArrayList;
import java.util.List;

/**
 * A class to log Lucene search queries (context, search parameters and search results); this class
 * is built with the GeonetContext object. If the settings has true for the value:
 * searchStats/enable, log operations will be performed, otherwise nothing will be done and a
 * notice-level log will be made
 *
 * @author nicolas Ribot
 */
public class SearcherLogger {
    @Value("${es.index.searchlogs}")
    private String index = "searchlogs";

    @Value("${es.index.searchlogs.type}")
    private String indexType = "searchlogs";

    public String getIndex() {
        return index;
    }
    public String getIndexType() {
        return indexType;
    }

    public void setIndex(String index) {
        this.index = index;
    }
    public void setIndexType(String indexType) {
        this.indexType = indexType;
    }

    /**
     * Constructor.
     */
    public SearcherLogger() {
    }

    /**
     * TODO javadoc.
     */
    public void logSearch(ServiceContext srvContext, Query query, int numHits, Sort sort, String geomFilterWKT, String guiService) {

        try {
            if (Log.isDebugEnabled(Geonet.SEARCH_LOGGER))
                Log.debug(Geonet.SEARCH_LOGGER, "Opening dbms...");

            if (query == null) {
                if (Log.isDebugEnabled(Geonet.SEARCH_LOGGER))
                    Log.debug(Geonet.SEARCH_LOGGER, "Null Query object. cannot log search operation");
                return;
            }

            QueryRequest queryRequest = new QueryRequest(srvContext.getIpAddress(), (new java.util.Date()).getTime());
            List<SearchRequestParam> queryInfos = extractQueryTerms(query);
            // type is also set when doing this.
            queryRequest.setQueryInfos(queryInfos);
            queryRequest.setHits(numHits);
            queryRequest.setService(srvContext.getService());
            queryRequest.setLanguage(srvContext.getLanguage());
            queryRequest.setLuceneQuery(query.toString());
            // sortBy, spatial filter ?
            if (sort != null) queryRequest.setSortBy(concatSortFields(sort.getSort()));
            queryRequest.setSpatialFilter(geomFilterWKT);
            // sets the simple type through this call...
            queryRequest.isSimpleQuery();
            queryRequest.setAutoGeneratedQuery(guiService.equals("yes"));

            if (!queryRequest.storeToEs(index, indexType)) {
                Log.warning(Geonet.SEARCH_LOGGER, "unable to log query into database...");
            } else {
                if (Log.isDebugEnabled(Geonet.SEARCH_LOGGER))
                    Log.debug(Geonet.SEARCH_LOGGER, "Query saved to database");
            }
        } catch (Exception e) {
            // I dont want the log to cause an exception and hide the real problem.
            Log.error(Geonet.SEARCH_LOGGER, "Error logging search: " + e.getMessage(), e);
        }
    }

    /**
     * Returns a dictionary containing field/text for the given query.
     *
     * @param query The query to process to extract terms and
     * @return a Hashtable whose key is the field and the value is the text
     */
    protected List<SearchRequestParam> extractQueryTerms(Query query) {
        if (query == null) {
            return null;
        }
        List<SearchRequestParam> result = new ArrayList<SearchRequestParam>();

        BooleanClause[] clauses;

        if (query instanceof BooleanQuery) {
            BooleanQuery bq = (BooleanQuery) query;
            clauses = bq.getClauses();

            for (BooleanClause clause : clauses) {
                result.addAll(extractQueryTerms(clause.getQuery()));
            }
        } else {
            Optional<List<SearchRequestParam>> info = LuceneQueryParamType.createRequestParam(query);
            if (info.isPresent()) {
                for (SearchRequestParam s : info.get()) {
                    result.add(s);
                }
            } else {
                Log.debug(Geonet.SEARCH_LOGGER, "unknown queryInfo type: " + query.getClass().getName());
            }
        }

        return result;
    }

    /**
     * Concatenates the given terms' text into a single String, with the given separator.
     *
     * @param terms     the set of terms to concatenate
     * @param separator the separator to use to separate text elements (use ',' if sep is null)
     * @return a string containing all this terms' texts concatenated
     */
    private String concatTermsText(Term[] terms, String separator) {
        if (terms == null || separator == null) {
            return null;
        }

        StringBuilder sb = new StringBuilder();
        for (Term t : terms) {
            sb.append(t.text()).append(separator);
        }
        if (sb.length() > 0) {
            sb.deleteCharAt(sb.length() - 1);
        }
        return sb.toString();
    }

    /**
     * Concatenates the given terms' fields into a single String, with the given separator.
     *
     * @param terms     the set of terms to concatenate
     * @param separator the separator to use to separate text elements  (use ',' if sep is null)
     * @return a string containing all this terms' fields concatenated
     */
    private String concatTermsField(Term[] terms, String separator) {
        if (terms == null || separator == null) return null;

        StringBuilder sb = new StringBuilder();
        for (Term t : terms) {
            sb.append(t.field()).append(separator);
        }
        if (sb.length() > 0) {
            sb.deleteCharAt(sb.length() - 1);
        }
        return sb.toString();
    }

    /**
     * Concatenates the given sortFields into a single string of the form. The concatenation will
     * lead to a string: <field> ASC|DESC,<field> ASC|DESC,... where <field> is the sort field name
     * and ASC means ascending sort; DESC means descending sort (reverse sort)
     *
     * @param sortFields the array of fields to concatenate
     * @return a string containing fields concatenated.
     */
    private String concatSortFields(SortField[] sortFields) {
        if (sortFields == null) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        for (SortField sf : sortFields) {
            if (sf != null) {
                sb.append(sf.getField()).append(" ").append(sf.getReverse() ? "DESC" : "ASC").append(",");
            }
        }
        if (sb.length() > 0) {
            sb.deleteCharAt(sb.length() - 1);
        }
        return sb.toString();
    }
}
